package com.dragon.modules.yuegou.utils;

import java.util.ArrayList;
import java.util.Collection;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.function.BiConsumer;
import java.util.function.Function;
import java.util.function.UnaryOperator;
import java.util.stream.Collectors;

/**
 * 常用函数工具类
 * 
 * @author liuwu
 *
 */
public class Functions {

	/**
	 * 给集合中的每个元素绑定属性<br>
	 * 应用场景：1+N查询
	 * <p>
	 * 示例：有Student和School两个对象，其中Student记录了schoolId，要通过schoolId关联设置School对象：
	 * <pre>
	 * class Student{
	 *     Integer id;
	 *     String name;
	 *     Integer schoolId;
	 *     School school;
	 *     // 省略getters/setters
	 * }
	 * class School{
	 *     Integer id;
	 *     String name;
	 *     // 省略getters/setters
	 * }
	 * interface SchoolRepository{
	 *     School getById(Integer id);
	 * }
	 * </pre>
	 * 
	 * 调用这个方法给每个Student都关联School：
	 * <pre>
	 * Functions.attach(students, Student::getSchoolId, schoolRepository::getById, Student::setSchool);
	 * </pre>
	 * 
	 * 其结果与以下代码逻辑上是等价的：
	 * <pre>
	 * for(Student student : students) {
	 *     Integer schoolId = student.getSchoolId();
	 *     School school = schoolRepository.getById(schoolId);
	 *     if(school != null) {
	 *         student.setSchool(school);
	 *     }
	 * });
	 * </pre>
	 * 
	 * 但是对性能上进行了优化：
	 * <ol>
	 * <li>并行开启查询</li>
	 * <li>不会重复查询</li>
	 * <ol>
	 * 
	 * @param <E>         元素类型
	 * @param <K>         键类型
	 * @param <V>         值类型
	 * @param coll        集合
	 * @param keyFunction 获取元素键的函数
	 * @param valueLoader 根据键获取值的函数，通常来说这是一个比较重的函数，例如从数据库中查询数据，调用API查询数据
	 * @param valueSetter 设置元素值的函数
	 */
	public static <E, K, V> void attach(Collection<E> coll, Function<E, K> keyFunction, Function<K, V> valueLoader,
			BiConsumer<E, V> valueSetter) {
		Set<K> keyset = coll.stream().map(keyFunction).filter(k -> k != null).collect(Collectors.toSet());
		if (!keyset.isEmpty()) {
			Map<K, Optional<V>> valueMap = new ArrayList<>(keyset).parallelStream()
					.collect(Collectors.toMap(UnaryOperator.identity(), k -> Optional.ofNullable(valueLoader.apply(k))));
			coll.stream().forEach(element -> {
				K key = keyFunction.apply(element);
				Optional<V> value = valueMap.get(key);
				if (Boolean.TRUE.equals(value.isPresent())) {
					valueSetter.accept(element, value.get());
				}
			});
		}
	}

	/**
	 * 给集合中的每个元素绑定属性<br>
	 * 应用场景：1+N查询，把1+N查询转化为1+1查询
	 * <p>
	 * 示例：有Student和School两个对象，其中Student记录了schoolId，要通过schoolId关联设置School对象：
	 * <pre>
	 * class Student{
	 *     Integer id;
	 *     String name;
	 *     Integer schoolId;
	 *     School school;
	 *     // 省略getters/setters
	 * }
	 * class School{
	 *     Integer id;
	 *     String name;
	 *     // 省略getters/setters
	 * }
	 * interface SchoolRepository{
	 *     Map&lt;Integer, School&gt; getByIds(Collection&lt;Integer&gt; ids);
	 * }
	 * </pre>
	 * 
	 * 调用这个方法给每个Student都关联School：
	 * <pre>
	 * Functions.attachBatch(students, Student::getSchoolId, schoolRepository::getByIds, Student::setSchool);
	 * </pre>
	 * 
	 * 其结果与以下代码逻辑上是等价的：
	 * <pre>
	 * Set&lt;Integer&gt; schoolIds = new HashSet&lt;&gt;();
	 * for(Student student : students) {
	 *     if(student.getSchoolId() != null) {
	 *         schoolIds.add(student.getSchoolId());
	 *     }
	 * }
	 * Map&lt;Integer, School&gt; schoolMap = schoolRepository.getByIds(schoolIds);
	 * for(Student student : students) {
	 *     if(student.getSchoolId() != null) {
	 *         School school = schoolMap.get(student.getSchoolId());
	 *         if(school != null) {
	 *             student.setSchool(school);
	 *         }
	 *     }
	 * }
	 * </pre>
	 * 
	 * @param <E>              元素类型
	 * @param <K>              键类型
	 * @param <V>              值类型
	 * @param coll             集合
	 * @param keyFunction      获取元素键的函数
	 * @param batchValueLoader 根据键集合获取键值映射表的函数，通常来说这是一个比较重的函数，例如从数据库中查询数据，调用API查询数据
	 * @param valueSetter      设置元素值的函数
	 */
	public static <E, K, V> void attachBatch(Collection<E> coll, Function<E, K> keyFunction,
			Function<Collection<K>, Map<K, V>> batchValueLoader, BiConsumer<E, V> valueSetter) {
		Set<K> keyset = coll.stream().map(keyFunction).filter(k -> k != null).collect(Collectors.toSet());
		if (!keyset.isEmpty()) {
			Map<K, V> valueMap = batchValueLoader.apply(keyset);
			coll.stream().forEach(element -> {
				K key = keyFunction.apply(element);
				V value = valueMap.get(key);
				if (value != null) {
					valueSetter.accept(element, value);
				}
			});
		}
	}

}
